﻿using System;
using System.Reflection;
using System.Xml.Linq;

namespace CodingWheels.DataTypes.Core
{
	public abstract class DataType<T, TUnit> : IComparable<T>, IEquatable<T>
		where T : DataType<T, TUnit>, new() where TUnit : Unit
	{
		private const string ValueAttributeName = "value";
		private const string UnitAttributeName = "unit";

		private static readonly Type CdtType = typeof(T);

		private readonly IConverter<TUnit> _converter = ConverterFactory.GetConverter<TUnit>();

		public double Value
		{
			get;
			private set;
		}

		public TUnit Unit
		{
			get;
			private set;
		}

		protected DataType(double value, TUnit unit)
		{
			Value = value;
			Unit = unit;
		}

		public override bool Equals(object obj)
		{
			if(ReferenceEquals(null, obj))
			{
				return false;
			}
			return obj.GetType() == CdtType && Equals((T)obj);
		}

		public override int GetHashCode()
		{
			return Value.GetHashCode() + Unit.GetHashCode();
		}

		public int CompareTo(T other)
		{
			return Value.CompareTo(other.In(Unit));
		}

		public bool Equals(T other)
		{
			if(ReferenceEquals(null, other))
			{
				return false;
			}

			return ReferenceEquals(this, other) || Value == other.In(Unit);
		}

		public override string ToString()
		{
			return string.Format("{0:N3} {1}", Value, Unit);
		}

		public string ToXml()
		{
			var xElement = new XElement(CdtType.Name);

			xElement.Add(new XAttribute(ValueAttributeName, Value));
			xElement.Add(new XAttribute(UnitAttributeName, Unit));

			return xElement.ToString();
		}

		public double In(TUnit unit)
		{
			return _converter.Convert(Value, Unit, unit);
		}

		public void SetIn(double value, TUnit unit)
		{
			Value = value;
			Unit = unit;
		}

		public void Add(T value)
		{
			Value += value.In(Unit);
		}

		public void Add(double value, TUnit unit)
		{
			Value += _converter.Convert(value, unit, Unit);
		}

		public void Subtract(T value)
		{
			Value -= value.In(Unit);
		}

		public void Subtract(double value, TUnit unit)
		{
			Value -= _converter.Convert(value, unit, Unit);
		}

		public void MultiplyBy(double factor)
		{
			Value *= factor;
		}

		public void DivideBy(double divisor)
		{
			Value /= divisor;
		}

		public T ConvertTo(TUnit unit)
		{
			return CreateNewComplexDataType(In(unit), unit);
		}

		public static T Parse(string value)
		{
			try
			{
				var stringArray = value.Split(new[] { ' ' });

				return CreateNewComplexDataType(double.Parse(stringArray[0]), GetUnit(stringArray[1]));
			}
			catch(Exception)
			{
				throw new ArgumentException("Input string is not properly formatted.");
			}
		}

		public static T Parse(string value, TUnit unit)
		{
			return CreateNewComplexDataType(double.Parse(value), unit);
		}

		public static T ParseXml(string xml)
		{
			try
			{
				var xElement = XElement.Parse(xml);

				if(xElement.Name != CdtType.Name)
				{
					throw new ArgumentException(string.Format("Input is not a valid XML string for type of {0}.", CdtType.Name));
				}

				var valueAttribute = xElement.Attribute(ValueAttributeName);
				var unitAttribute = xElement.Attribute(UnitAttributeName);

				if(valueAttribute == null || unitAttribute == null)
				{
					throw new ArgumentException("Input is not a valid XML string.");
				}

				var value = double.Parse(valueAttribute.Value);
				var unit = GetUnit(unitAttribute.Value);

				return CreateNewComplexDataType(value, unit);
			}
			catch(Exception)
			{
				throw new ArgumentException("Input is not a valid XML string.");
			}
		}

		public static T operator +(DataType<T, TUnit> left, T right)
		{
			return CreateNewComplexDataType(left.Value + right.In(left.Unit), left.Unit);
		}

		public static T operator -(DataType<T, TUnit> left, T right)
		{
			return CreateNewComplexDataType(left.Value - right.In(left.Unit), left.Unit);
		}

		public static T operator *(DataType<T, TUnit> left, double factor)
		{
			return CreateNewComplexDataType(left.Value * factor, left.Unit);
		}

		public static T operator *(double factor, DataType<T, TUnit> right)
		{
			return right * factor;
		}

		public static T operator /(DataType<T, TUnit> left, double divisor)
		{
			return CreateNewComplexDataType(left.Value / divisor, left.Unit);
		}

		public static double operator /(DataType<T, TUnit> left, T right)
		{
			return left.Value / right.In(left.Unit);
		}

		public static bool operator ==(DataType<T, TUnit> left, T right)
		{
			if(((object)left == null) || ((object)right == null))
			{
				return false;
			}

			return left.Equals(right);
		}

		public static bool operator !=(DataType<T, TUnit> left, T right)
		{
			if(((object)left == null) || ((object)right == null))
			{
				return false;
			}

			return !left.Equals(right);
		}

		public static bool operator <(DataType<T, TUnit> left, T right)
		{
			return left.CompareTo(right) < 0;
		}

		public static bool operator >(DataType<T, TUnit> left, T right)
		{
			return left.CompareTo(right) > 0;
		}

		public static bool operator <=(DataType<T, TUnit> left, T right)
		{
			return left.CompareTo(right) <= 0;
		}

		public static bool operator >=(DataType<T, TUnit> left, T right)
		{
			return left.CompareTo(right) >= 0;
		}

		public static TUnit GetUnit(string unitName)
		{
			var fieldInfo = typeof(TUnit).GetField(unitName, BindingFlags.Public | BindingFlags.Static);

			return (TUnit)fieldInfo.GetValue(null);
		}

		private static T CreateNewComplexDataType(double value, TUnit unit)
		{
			var t = new T();
				
			t.SetIn(value, unit);

			return t;
		}
	}
}